﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web.Script.Serialization;
using PostSharp.Aspects;
using PostSharp.Extensibility;

namespace UsageTracker.Logger
{
    using System.Diagnostics;
    using System.Globalization;
    using System.Management;
    using System.Net.NetworkInformation;
    using System.Text;

    using UsageTracker.Logger.DataObjects;
    using UsageTracker.Logger.Loggers;

    /// <summary>
    /// This is a custom "class attribute" class which leverages AOP features provided by the free edition of Postsharp.NET.
    /// The basic purpose of this attribute is to intercept a decorated method call and extract the method/process/argument information for logging purposes. 
    /// Usage of this attribute requires Postsharp binaries to be referenced in each project that uses this attribute and also include a Postsharp processing step
    /// in the build process so that the aspect can be injected into the target classes at compile time.
    /// In order to make sure that Postsharp processing is done correctly, check the MSBuild run log in non-minimal verbosity and look for a Postsharp log event indicating
    /// that Postsharp has run and processed the assemblies.
    /// The class makes use of configuration parameters to be defined in the host application's .CONFIG file, AppSettings section to define the logging parameters and 
    /// other key information. While it is highly recommended to provide this information in the specific config file, the tool defaults to some average numbers for log flushing 
    /// parameters in the absence of those keys in config file. 
    /// NOTE- In case you are using the ReST logging mode, the ReST logging endpoint URI is a mandatory setting to be provided through the config.
    /// </summary>
    [MulticastAttributeUsage(MulticastTargets.Method, AllowMultiple = true, AllowExternalAssemblies = true, PersistMetaData = false, TargetMemberAttributes = MulticastAttributes.NonAbstract)]
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    [Serializable]
    public class UsageCapture : MethodInterceptionAspect
    {
        /// <summary>
        /// Name of the product for which usage is being logged
        /// </summary>
        private string productName;

        /// <summary>
        /// Field for holding any custom data supplied externally for logging
        /// </summary>
        private string usageData;

        /// <summary>
        /// Interface for the external logging implementation
        /// </summary>
        private static IUsageLogger logger;

        /// <summary>
        /// Flag to indicate whether machine and process details are captured in log
        /// </summary>
        private bool isMachineProcessDetailsCaptured;

        /// <summary>
        /// Flag to indicate whether method level arguments are captured in log
        /// </summary>
        private bool isMethodArgsCaptured;

        /// <summary>
        /// Flag to indicate that the log item is to be flushed immediately
        /// </summary>
        private bool isLogImmediate;

        /// <summary>
        /// Default .ctor
        /// </summary>
        /// <param name="productName">Name of the product being monitored</param>
        /// <param name="isLogImmediate">Boolean. Indicates if the log item is to be flushed immediately</param>
        public UsageCapture(string productName, bool isLogImmediate = false)
        {
            this.productName = productName;
            this.isLogImmediate = isLogImmediate;
        }

        /// <summary>
        /// Constructor for allowing users to include machine and process level details in the captured log information.
        /// </summary>
        /// <param name="productName">Name of the product being monitored</param>
        /// <param name="includeMachineProcessDetails">Default is False. A True value indicates that the machine and process level details should be captured.</param>
        /// <param name="isLogImmediate">Boolean. Indicates if the log item is to be flushed immediately</param>
        public UsageCapture(string productName, bool includeMachineProcessDetails, bool isLogImmediate = false)
        {
            this.productName = productName;
            this.isMachineProcessDetailsCaptured = includeMachineProcessDetails;
            this.isLogImmediate = isLogImmediate;
        }

        /// <summary>
        /// Constructor for providing custom user data as plain string for persistence. 
        /// </summary>
        /// <param name="productName">Name of the product being monitored</param>
        /// <param name="usageUserData">Custom user data as plain string for persistence. No processing is done on this string.</param>
        /// <param name="isLogImmediate">Boolean. Indicates if the log item is to be flushed immediately</param>
        public UsageCapture(string productName, string usageUserData, bool isLogImmediate = false)
        {
            this.productName = productName;
            this.usageData = usageUserData;
            this.isLogImmediate = isLogImmediate;
        }

        /// <summary>
        /// Constructor for allowing users to include machine and process level details in the captured log information and providing custom user data as plain string for persistence
        /// </summary>
        /// <param name="productName">Name of the product being monitored</param>
        /// <param name="includeMachineProcessDetails">Default is False. A True value indicates that the machine and process level details should be captured.</param>
        /// <param name="usageUserData">Custom user data as plain string for persistence. No processing is done on this string.</param>
        /// <param name="isLogImmediate">Boolean. Indicates if the log item is to be flushed immediately</param>
        public UsageCapture(string productName, bool includeMachineProcessDetails, string usageUserData, bool isLogImmediate = false)
        {
            this.productName = productName;
            this.isMachineProcessDetailsCaptured = includeMachineProcessDetails;
            this.usageData = usageUserData;
            this.isLogImmediate = isLogImmediate;
        }

        /// <summary>
        /// Constructor for allowing users to include machine and process level details in the captured log information, providing custom user data as plain string for persistence and method argument values
        /// </summary>
        /// <param name="productName">Name of the product being monitored</param>
        /// <param name="includeMachineProcessDetails">Includes data about the hosting machine and Windows process</param>
        /// <param name="usageUserData">Custom logging data</param>
        /// <param name="includeMethodArguments">Includes argument information for the invoked method in the log</param>
        /// <param name="isLogImmediate">Boolean. Indicates if the log item is to be flushed immediately</param>
        public UsageCapture(string productName, bool includeMachineProcessDetails, string usageUserData, bool includeMethodArguments, bool isLogImmediate = false)
        {
            this.productName = productName;
            this.isMachineProcessDetailsCaptured = includeMachineProcessDetails;
            this.usageData = usageUserData;
            this.isMethodArgsCaptured = includeMethodArguments;
            this.isLogImmediate = isLogImmediate;
        }

        /// <summary>
        /// Intercepts the method call and provides users an opportunity to inject their custom code.
        /// </summary>
        /// <param name="args">Arguments encapsulating the method invocation information</param>
        public override void OnInvoke(MethodInterceptionArgs args)
        {
            if (this.IsLoggingConfigurationOk())
            {
                //Create an async. operation for logging
                if (isLogImmediate)
                {
                    //In case of an immediate operation, do it synchronously
                    this.LogUsageData(args);
                }
                else
                {
                    Task.Factory.StartNew(this.LogUsageData, args);
                }

            }
            else
            {
#if DEBUG
                //This can be enhanced to log a message in the Windows Event Log of the consumer.
                Debug.Print("* Usage Logger Error * - The endpoint specified for the ReST logger is not valid. Cannot continue logging.");
#endif
            }

            base.OnInvoke(args);
        }

        /// <summary>
        /// AM: IMP - Test if the logging mode is set to ReST. If yes, check if a valid ReST Uri is specified. If yes, then go ahead with logging.
        /// If not, then log a debug message in DEBUG compilation and continue without instantiating logging components thus saving processor cycles and memory.
        /// </summary>
        /// <returns>True if ReST endpoint specs are provided correctly. </returns>
        private bool IsLoggingConfigurationOk()
        {
            bool isOk = false;

            if (ConfigurationManager.AppSettings.AllKeys.Contains("USAGETRACK_LOGGER_TYPE"))
            {
                if (ConfigurationManager.AppSettings.Get("USAGETRACK_LOGGER_TYPE")
                    .Equals("REST", StringComparison.InvariantCultureIgnoreCase))
                {
                    if (ConfigurationManager.AppSettings.AllKeys.Contains("USAGETRACK_REST_ENDPOINT"))
                    {
                        try
                        {
                            Uri loggerUri = new Uri(ConfigurationManager.AppSettings.Get("USAGETRACK_REST_ENDPOINT"));
                            isOk = true;
                        }
                        catch (Exception)
                        {
                            //Do Nothing
                        }
                    }
                }
                else
                {
                    isOk = true;
                }
            }
            return isOk;
        }

        /// <summary>
        /// Configures the data logger and serializes the data into JSON for transmission/persistence
        /// </summary>
        /// <param name="arguments">Arguments from the method invocation, converted to Object for async. calls</param>
        private void LogUsageData(object arguments)
        {
            //Set up the logger for recording
            ConfigureLogger();

            //Get a handle to the invoked method's arguments
            MethodInterceptionArgs args = arguments as MethodInterceptionArgs;

            //Instantiate the usage data object
            UsageData usageDo = new UsageData();

            //Set the product name field for recording. In case no product name is supplied, default to the executable name
            usageDo.ProductName = (String.IsNullOrEmpty(this.productName))
                ? AppDomain.CurrentDomain.FriendlyName
                : this.productName;
            usageDo.ProdExecutable = AppDomain.CurrentDomain.FriendlyName;
            usageDo.MachineName = Dns.GetHostName() + "." + IPGlobalProperties.GetIPGlobalProperties().DomainName;

            //Caller selected to use machine and process details
            if (this.isMachineProcessDetailsCaptured)
            {
                List<string> nicInfo = new List<string>();
                NetworkInterface.GetAllNetworkInterfaces().ToList().ForEach(
                    n => nicInfo.Add(n.Name + "_" + n.OperationalStatus + "_" + n.Speed));
                usageDo.NiCs = nicInfo.ToArray();
                usageDo.InstalledRam = GetInstalledRam();
                usageDo.LogicalDrives = Environment.GetLogicalDrives();
                usageDo.LogonDc = Environment.GetEnvironmentVariable("LOGONSERVER");
                usageDo.TcpConnCount = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections().Count();
                usageDo.HasOsShutdownStarted = Environment.HasShutdownStarted;
                usageDo.ProcessId = Process.GetCurrentProcess().Id;
                usageDo.Is64BitProcess = Environment.Is64BitProcess;
                usageDo.Is64BitOs = Environment.Is64BitOperatingSystem;
                usageDo.MachineOs = Environment.OSVersion.Platform + "_" + Environment.OSVersion.ServicePack + "_" + Environment.OSVersion.VersionString;
                usageDo.WorkingSet = Environment.WorkingSet;
                usageDo.UserName = Environment.UserDomainName + @"\" + Environment.UserName;
                usageDo.ProcCount = Environment.ProcessorCount;
                List<string> hostIps = new List<string>();
                usageDo.PublicIp = "74.125.224.224"; //GetPublicIp();
                Dns.GetHostAddresses(usageDo.MachineName).ToList().ForEach(i => hostIps.Add(i.ToString()));
                usageDo.MachineIPs = hostIps.ToArray();
            }

            //Caller supplied custom data
            if (this.usageData != null)
            {
                usageDo.CustomData = this.usageData.Split(",".ToCharArray());
            }

            if (args != null)
            {
                usageDo.Module = args.Method.Module.Name;
                usageDo.ModVersion = args.Method.Module.Assembly.GetName().Version.ToString();
                usageDo.Method = args.Method.Name;
                if (this.isMethodArgsCaptured)
                {
                    usageDo.MethodArgs = args.Arguments.Count > 0 ? args.Arguments.ToArray<object>() : null;
                }
            }

            //AM: IMP - All events are recorded in UTC time for uniform reporting
            usageDo.LogDateUtc = DateTime.Now.ToUniversalTime().Date.ToString(CultureInfo.InvariantCulture);

            //Use the JS serializer to create the JSON data string
            JavaScriptSerializer json = new JavaScriptSerializer { RecursionLimit = 10 };

            //AM: IMP - A custom serializer is written for this purpose to ensure fields with null values are not serialized and bandwidth is saved in transmission
            json.RegisterConverters(new JavaScriptConverter[] { new UsageDataSerializer() });
            string preparedUsageData = json.Serialize(usageDo);
            logger.Log(preparedUsageData, isLogImmediate);
        }

        /// <summary>
        /// Queries a free external web service to get the public IP of the hist computer. This data is logged for geolocation on the reporting side.
        /// </summary>
        /// <returns></returns>
        private string GetPublicIp()
        {
            string pubIp = String.Empty;
            using (WebClient client = new WebClient())
            {
                client.Encoding = Encoding.UTF8;
                client.Headers.Add(HttpRequestHeader.ContentType, "text/json");
                try
                {
                    pubIp = client.DownloadString(new Uri("http://ip.telize.com/"));
                    pubIp = pubIp.TrimEnd(Environment.NewLine.ToCharArray());
                }
                catch (Exception exp)
                {
#if DEBUG
                    Debug.Print(
                        "* Usage Data Logger Error * - Could not fetch the external/public IP for the current network environment. "
                        + exp.Message);
#endif
                }
            }
            return pubIp;
        }

        /// <summary>
        /// Get the installed RAM from the local machine using WMI
        /// </summary>
        /// <returns></returns>
        private string GetInstalledRam()
        {
            ConnectionOptions connection = new ConnectionOptions();
            connection.Impersonation = ImpersonationLevel.Impersonate;
            string memStr = String.Empty;
            try
            {
                ManagementScope scope = new ManagementScope("\\\\.\\root\\CIMV2", connection);
                scope.Connect();
                ObjectQuery query = new ObjectQuery("SELECT * FROM Win32_PhysicalMemory");
                ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope, query);
                foreach (ManagementObject queryObj in searcher.Get())
                {
                    memStr += queryObj["Capacity"] + " ";
                }
                query = null;
                searcher.Dispose();
                scope = null;
            }
            catch (Exception exp)
            {
                Console.WriteLine(exp.ToString());
            }
            finally
            {
                connection = null;
            }
            return memStr;
        }

        /// <summary>
        /// Reads the host config file and configures the data logger to be used. The choices are CONSOLE, FILE and REST (default)
        /// Once the data logger is ascertained, the instance of the logger is cached in the AppDomain storage for faster access.
        /// In case the logger type is changed while the application is running, the AppDomain based logger is not updated hence an app. 
        /// restart is required to make the new settings take effect.
        /// This method expects an application setting named "USAGETRACK_LOGGER_TYPE" in the host config file.
        /// </summary>
        private static void ConfigureLogger()
        {
            //Figure out the logger type to instantiate. If the logger object is already in AppDomain storage, retrieve and re-use.
            object cachedLoggerType = AppDomain.CurrentDomain.GetData("USAGETRACK_LOGGER_TYPE");
            if (cachedLoggerType != null)
            {
                logger = cachedLoggerType as IUsageLogger;
            }
            else
            {
                string loggerType = String.Empty;
                //Check if the logger type is explicitly specified in the config file. If not, default to the ReST logger.
                if (ConfigurationManager.AppSettings.AllKeys.Contains("USAGETRACK_LOGGER_TYPE"))
                {
                    loggerType = ConfigurationManager.AppSettings.Get("USAGETRACK_LOGGER_TYPE");
                }
                switch (loggerType.ToUpper())
                {
                    case "FILE":
                        logger = new FileLogger();
                        break;

                    case "CONSOLE":
                        logger = new ConsoleLogger();
                        break;

                    default:
                    case "REST":
                        logger = ReStLogger.Instance;
                        break;
                }
                //Save the current logger object to the Application Domain scoped storage for caching.
                AppDomain.CurrentDomain.SetData("USAGETRACK_LOGGER_TYPE", logger);
            }
        }
    }
}
